{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12234af9-4cc3-46a7-8880-1cb68aa7d9da",
   "metadata": {},
   "outputs": [],
   "source": [
    "# File: woven.ipynb\n",
    "# Code: Claude Code and Codex\n",
    "# Review: Ryoichi Ando (ryoichi.ando@zozo.com)\n",
    "# License: Apache v2.0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c8313e6-3512-49fd-9617-268ee0bd0f07",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "import numpy as np\n",
    "from frontend import App\n",
    "\n",
    "\n",
    "# function to generate woven cylinder structure with interlaced strands\n",
    "def make_woven_cylinder(n: int, offset: float, scale: float):\n",
    "    dx, width = 1.0 / (n - 1), 1.25\n",
    "    scale = 2.0 * 1.48 * scale\n",
    "    v_steps = int(25.0 * scale)\n",
    "    sep, strands = 0.5, []\n",
    "\n",
    "    # create vertical strands\n",
    "    for i in range(v_steps):\n",
    "        theta = 2.0 * np.pi * i / v_steps\n",
    "        xyz = np.zeros((n, 3))\n",
    "        xyz[:, 0] = width * (2.0 * dx * np.arange(n) - 1.0)\n",
    "        xyz[:, 1], xyz[:, 2] = sep * np.sin(theta), sep * np.cos(theta)\n",
    "        strands.append((xyz, False))\n",
    "\n",
    "    # create horizontal ring strands with wave pattern\n",
    "    h_steps = int(30.0 * scale)\n",
    "    ring_steps = v_steps * 3\n",
    "    assert ring_steps % 2 == 0, \"ring_steps must be even\"\n",
    "    amp, dx_h, half_v = 1.2 * offset, 1.0 / (h_steps - 1), v_steps // 2\n",
    "\n",
    "    for i in range(1, h_steps - 1):\n",
    "        sgn = 1.0 if (i % 2 == 0) else -1.0\n",
    "        xyz = np.zeros((ring_steps, 3))\n",
    "        xyz[:, 0] = width * (2.0 * dx_h * i - 1.0)\n",
    "        j_indices = np.arange(ring_steps)\n",
    "        theta_vals = 2.0 * np.pi * j_indices / ring_steps\n",
    "        r = sep + sgn * amp * np.cos(half_v * theta_vals)\n",
    "        xyz[:, 1], xyz[:, 2] = r * np.sin(theta_vals), r * np.cos(theta_vals)\n",
    "        strands.append((xyz, True))\n",
    "\n",
    "    return strands\n",
    "\n",
    "\n",
    "# create an app\n",
    "app = App.create(\"woven\")\n",
    "\n",
    "# create a scene\n",
    "scene = app.scene.create()\n",
    "\n",
    "# generate and add all woven strands\n",
    "angular_vel, move_delta, t_end = 360 + 10 * random.random(), 0.15, 10\n",
    "for k, (V, closed) in enumerate(make_woven_cylinder(256, 4e-3, 2.0)):\n",
    "    # create edge connectivity for rod strands\n",
    "    E = [[i, i + 1] for i in range(len(V) - 1)]\n",
    "    if closed:\n",
    "        E.append([len(V) - 1, 0])\n",
    "    name = f\"strand-{k}\"\n",
    "    app.asset.add.rod(name, V, np.array(E, dtype=np.uint32))\n",
    "    obj = scene.add(name)\n",
    "    # set material properties for woven strands\n",
    "    (\n",
    "        obj.param.set(\"bend\", 1e-3)\n",
    "        .set(\"young-mod\", 1e5)\n",
    "        .set(\"contact-gap\", 1.5e-3)\n",
    "        .set(\"contact-offset\", 2e-3)\n",
    "        .set(\"friction\", 0.01)\n",
    "        .set(\"length-factor\", 0.8)\n",
    "    )\n",
    "    # pin and animate open-ended strands\n",
    "    if not closed:\n",
    "        (\n",
    "            obj.pin(obj.grab([-1, 0, 0]))\n",
    "            .spin(axis=[1, 0, 0], angular_velocity=angular_vel)\n",
    "            .move_by([move_delta, 0, 0], 0, t_end)\n",
    "        )\n",
    "        (\n",
    "            obj.pin(obj.grab([1, 0, 0]))\n",
    "            .spin(axis=[-1, 0, 0], angular_velocity=angular_vel)\n",
    "            .move_by([-move_delta, 0, 0], 0, t_end)\n",
    "        )\n",
    "\n",
    "# compile the scene and report stats\n",
    "scene = scene.build().report()\n",
    "\n",
    "# preview the initial scene\n",
    "scene.preview()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07068865-0a55-412a-b9e9-663bf796133b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# create a new session with the compiled scene\n",
    "session = app.session.create(scene)\n",
    "\n",
    "# set session parameters - disable gravity and add air friction\n",
    "(\n",
    "    session.param.set(\"frames\", 450)\n",
    "    .set(\"dt\", 1 / 120)\n",
    "    .set(\"gravity\", 0.0)\n",
    "    .set(\"isotropic-air-friction\", 1e-3)\n",
    ")\n",
    "\n",
    "# build this session\n",
    "session = session.build()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c934643-8d74-4e04-ba83-ae4cee68e108",
   "metadata": {},
   "outputs": [],
   "source": [
    "# start the simulation and live-preview the results\n",
    "session.start().preview()\n",
    "\n",
    "# also show simulation logs in realtime\n",
    "session.stream()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "685409d0-381d-46ed-b10d-d2245bf56698",
   "metadata": {},
   "outputs": [],
   "source": [
    "# create an animation from the simulation results\n",
    "session.animate()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9fb98a8-1892-487c-a3de-2783f06eb87c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# export the animation to file\n",
    "session.export.animation()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d542a9ed-2e99-410a-9330-37c1e0af8270",
   "metadata": {},
   "outputs": [],
   "source": [
    "# this is for CI\n",
    "if app.ci:\n",
    "    assert session.finished()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
